/**
   @file disk.c
   @brief The disk workload.
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <math.h>
#include <pthread.h>
#include "cJSON.h"
#include "disk.h"
#include "random.h"
#include "resource_model.h"
#include "scheduler.h"
#include "generator.h"

/** The file permissions to use when creating a file to perform IO operations on. Largely irrelavant since the generator doesn't write any sensible data to the file nor do anything with data read from it. */
#define PERMS 0666

/** The max size of any single read or write to the disk.
    In order to minimize the effect of the disk workload on memory, the generator allocates a single piece of memory of size IO_BUFFER_SIZE for read and write operations. Jobs that read or write more memory than this size are broken up into multiple calls to read or write of size equal to or less than IO_BUFFER_SIZE. This is done to support arbitrarily large reads and writes for the disk workload without incurring too much cost in memory. The choice of 4KB is recommended by tests recorded in Figure 3.5 in the book, Advanced Programming in the Unix Environment by Stevens and Rago. */
#define IO_BUFFER_SIZE 4096

/** The hash table of modes supported by the disk workload. */
mode_definition_list *disk_modes[HASHSIZE];

/** Create and write the file to perform IO on of size specified in a JSON object. */
char * setup_file(cJSON *);

/** Spawn a disk job and schedule the next disk job arrival. */
void create_disk_workload(void *);

/** Redeclaring the standard read function. Required by run_disk_io. */
int read(int, char*,int);

/** Redeclaring the standard write function. Required by run_disk_io. */
int write(int, char*,int);

/** Setup a mode to randomly read from the disk.*/
mode * setup_disk_read_random(cJSON *);

/** Setup a mode to randomly write to the disk. */
mode * setup_disk_write_random(cJSON *);

/** Setup a mode to sequentially read from the disk.*/
mode * setup_disk_read_sequential(cJSON *);

/** Setup a mode to sequentially write to the disk. */
mode * setup_disk_write_sequential(cJSON *);

/** The buffer which stores memory read from and to be written to the disk. 
    This is required as read and write both need a piece of memory to read from and write to. No attempt is made to make access to this buffer thread safe since the state of the buffer is of no concern to the disk workload. 
    It exists only to give a disk job some valid memory to perform disk IO with. It is of size IO_BUFFER_SIZE as that is the maximum amount of data that the generator  writes to or reads from the disk in one operation.  */
char io_buffer[IO_BUFFER_SIZE];

/** Reads and writes data to the disk */
int run_disk_io(int(*)(int,char*,int), int, unsigned long, unsigned long); 

/** Read bytes from the disk. 
    @param fd File descriptor to be read from
    @param pos Position in the file to start reading from
    @param n Number of bytes to be read from the disk.
*/
int read_bytes(int fd, long pos, int n)
{
  return run_disk_io(read, fd, pos, n);
}

/** Write bytes to the disk
    @param fd File descriptor to be read from
    @param pos Position in the file to start writing from.
    @param n Number of bytes to write to the disk.
 */
int write_bytes(int fd, long pos, int n)
{
  return run_disk_io(write, fd, pos, n);
}

int run_disk_io(int(*function)(int, char *, int), int fd, unsigned long pos, unsigned long n) {
  if (lseek(fd, pos, 0) >= 0) {
    unsigned long steps = n / IO_BUFFER_SIZE;
    unsigned long runoff = n % IO_BUFFER_SIZE;
    unsigned long result = 0;
    int i = 0;
    for(i; i <= steps; i++) {
      unsigned long bytes = (i == steps) ? runoff : IO_BUFFER_SIZE;
      if( bytes > 0) {
        result += function(fd,io_buffer,bytes);
      }
    }
    return result;
  }
  else {
    perror("Failed to seek.\n");
    return -1;
  }
}

resource_model *setup_disk(cJSON *json) {
  install_mode(disk_modes,"wr",setup_disk_write_random);
  install_mode(disk_modes,"rr",setup_disk_read_random);
  install_mode(disk_modes,"rs",setup_disk_read_sequential);
  install_mode(disk_modes,"ws",setup_disk_write_sequential);
  cJSON *distribution = cJSON_GetObjectItem(json,"distribution");
  setup_rng("disk_arrival", distribution);
  char *filepath = setup_file(json);
  resource_model *model = malloc(sizeof(resource_model));
  model->birth = create_disk_workload;
  model->state = (void *) filepath;
  model->modes = initialize_modes(disk_modes, json);
  model->cnt_modes = count_modes(json);
  return model;
}

char *setup_file(cJSON *json) {
  cJSON *filesize = cJSON_GetObjectItem(json, "filesize");
  unsigned long i_filesize = filesize->valueint * 1000;
  char **buf = malloc(sizeof(char *));
  asprintf(buf, "../tmp/%lu", (unsigned long int)pthread_self()); /* Create a filename from the thread id */
  creat(*buf,0666);
  int file = open(*buf,O_WRONLY,0);
  write_bytes(file, 0, i_filesize);
  close(file);
  char **uniform = malloc(sizeof(char *));
  asprintf(uniform, "{\"type\":\"uniform\",\"min\":0,\"max\":%lu}", i_filesize);
  setup_rng("random_position_in_file",cJSON_Parse(*uniform));
  return *buf;
}

void create_disk_workload(void *args) {
  pthread_t *thread = spawn_job((resource_model *) args);
  free(thread);
  schedule_event(ms_from_now(get_random_number("disk_arrival")), create_disk_workload, args);
}

/** Read one byte at a time from the disk from random positions. */
void *disk_read_rand(void *args) {
  char *filepath = (char *)args;
  int fd = open(filepath,O_RDONLY,0);
  unsigned long position = 0;
  unsigned long bytes_to_read = (unsigned long) (get_random_number("disk_rr_length") * 1000);
  unsigned long steps = (unsigned long) floor(bytes_to_read / IO_BUFFER_SIZE);
  unsigned long runoff = bytes_to_read % IO_BUFFER_SIZE;
  int i = 0;
  for(i; i <= steps; i++){
    unsigned long to_read = (i == steps) ? runoff : IO_BUFFER_SIZE;
    position = (unsigned long) get_random_number("random_position_in_file");
    read_bytes(fd, position, to_read);
  }
  close(fd);
}

/** Write one byte at a time from the disk from random positions. */
void *disk_write_rand(void *args) {
  char *filepath = (char *)args;
  int fd = open(filepath,O_WRONLY,0);
  unsigned long bytes_to_write = (unsigned long) (get_random_number("disk_wr_length") * 1000);
  unsigned long position = 0;
  unsigned long steps = (unsigned long) floor(bytes_to_write / IO_BUFFER_SIZE);
  unsigned long runoff = bytes_to_write % IO_BUFFER_SIZE;
  int i = 0;
  for(i; i <= steps; i++){
    unsigned long to_write = (i == steps) ? runoff : IO_BUFFER_SIZE;
    position = (unsigned long) get_random_number("random_position_in_file");
    write_bytes(fd, position, to_write);
  }
  close(fd);
}

/** Read a random size of bytes in a row from a random position. */
void *disk_read_sequential(void *args) {
  char *filepath = (char *)args;
  int fd = open(filepath, O_RDONLY, 0);
  unsigned long position = (unsigned long) get_random_number("random_position_in_file");
  unsigned long bytes_to_read = (unsigned long) (get_random_number("disk_rs_length") * 1000);
  int written = read_bytes(fd, position, bytes_to_read);
  printf("%d\n",written);
  close(fd);
}

/** Write a random size of bytes in a row from a random position. */
void *disk_write_sequential(void *args) {
  char *filepath = (char *)args;
  int fd = open(filepath, O_WRONLY, 0);
  unsigned long position = (unsigned long) get_random_number("random_position_in_file");
  unsigned long bytes_to_write = (unsigned long) (get_random_number("disk_ws_l") * 1000);
  write_bytes(fd, position, bytes_to_write);
  close(fd);
}

mode *setup_disk_read_random(cJSON *json) {
  setup_rng("disk_rr_length", cJSON_GetObjectItem(json,"length"));
  return new_mode("rr",disk_read_rand,json);
}

mode *setup_disk_write_random(cJSON *json) {
  setup_rng("disk_wr_length", cJSON_GetObjectItem(json,"length"));
  return new_mode("wr",disk_write_rand,json);
}

mode *setup_disk_read_sequential(cJSON *json) {
  setup_rng("disk_rs_length", cJSON_GetObjectItem(json,"length"));
  return new_mode("rs",disk_read_sequential,json);
}

mode *setup_disk_write_sequential(cJSON *json) {
  setup_rng("disk_ws_l", cJSON_GetObjectItem(json,"length"));
  return new_mode("ws",disk_write_sequential,json);
}
